﻿namespace KLF.Lib.Browser.Resources
{
    #region Using Statements

    using System;
    using System.IO;
    using System.Linq;
    using System.Text;
    using System.Collections.Generic;
    using System.Security.Cryptography;
    using System.Net;

    #endregion

    /// <summary>
    /// 
    /// </summary>
    public class CacheManager
    {
        class MemoryCacheEntry
        {
            public string Content { get; set; }

            public int Hits { get; set; }

            public DateTime LastHit { get; set; }
        }

        #region Constructors

        public CacheManager(string cacheDirectory, int maxSize, int maxMemSize, bool _init)
        {
            CacheDirectory = cacheDirectory;
            InitializeOnStartup = _init;
            MaximumDiskUsage = maxSize * 1024;
            MaximumMemoryUsage = maxMemSize * 1024;

            Dir = new DirectoryInfo(CacheDirectory);
            LoadedCache = new Dictionary<string, MemoryCacheEntry>();

            if (_init)
                InvaidateCacheState();
        }

        /// <summary>
        /// Constructs a new CacheManager object.
        /// </summary>
        /// <param name="cacheDirectory"></param>
        /// <param name="maxSize"></param>
        public CacheManager(string cacheDirectory, int maxSize)
            : this(cacheDirectory, maxSize, 0, false)
        {
        }

        public CacheManager(string cacheDirectory)
            : this(cacheDirectory, -1, 0, false)
        {
        }

        #endregion

        #region Attributes

        /// <summary>
        /// The base directory of the cache.
        /// </summary>
        public string CacheDirectory { get; set; }

        /// <summary>
        /// The instance of the DirectoryInfo that points to the root of the cache.
        /// </summary>
        private DirectoryInfo Dir { get; set; }

        /// <summary>
        /// Files that are considered part of the cache.
        /// </summary>
        private List<FileInfo> Files { get; set; }

        /// <summary>
        /// The flag indicating whether the cache should check the state
        /// of the disk on startup.
        /// </summary>
        public bool InitializeOnStartup { get; set; }

        /// <summary>
        /// Represents cache entries that are currentlo loaded into memory.
        /// </summary>
        private Dictionary<string, MemoryCacheEntry> LoadedCache { get; set; }

        /// <summary>
        /// The maximum size of the cache in KB.  -1 = grow forever.  Setting
        /// the MaximumDiskUsage to 0 will make this cache an in-memory only
        /// cache (must still specify a valid cache directory, though).
        /// </summary>
        public int MaximumDiskUsage { get; set; }

        /// <summary>
        /// The maximum size of the cache allowed in memory.  -1 = unlimited.
        /// </summary>
        public int MaximumMemoryUsage { get; set; }

        /// <summary>
        /// The space used in kilobytes (as of last check).
        /// </summary>
        public long DiskSpaceUsed { get; private set; }

        #endregion

        #region Methods

        private List<FileInfo> GetFiles()
        {
            Dir.Refresh();
            return (Files = new List<FileInfo>(Dir.GetFiles(".cache")));
        }

        /// <summary>
        /// Enumerate the files and make initial call to manage the cache size.
        /// </summary>
        public void InvaidateCacheState()
        {
            if (!String.IsNullOrEmpty(CacheDirectory))
            {
                if (!Dir.Exists)
                {
                    Dir.Create();
                    Dir.Refresh();
                }
                if (!Dir.Exists)
                    throw new DirectoryNotFoundException("Could not find nor create directory: " + CacheDirectory);

                GetFiles();

                DiskSpaceUsed = (from f in Files
                             select f.Length).Sum();

                if (MaximumDiskUsage != -1 && DiskSpaceUsed > MaximumDiskUsage)
                    ManageSize();
            }
        }

        /// <summary>
        /// Manage the size of the cache by deleting the oldest entries until
        /// we are under our maximum allotted size.
        /// </summary>
        private void ManageSize()
        {
            GetFiles();
            Files
                .Sort((f1, f2) =>
                {
                    return (f1.LastAccessTime < f2.LastAccessTime ? -1 :
                        (f1.LastAccessTime == f2.LastAccessTime ? 0 : -1));
                });

            while (DiskSpaceUsed > MaximumDiskUsage)
            {
                //  Determine oldest entry in cache
                var oldest = Files
                    .DefaultIfEmpty(null)
                    .FirstOrDefault(f => f.Exists);

                if (oldest != null)
                {
                    DiskSpaceUsed -= oldest.Length;

                    oldest.Delete();
                    oldest.Refresh();

                    if (oldest.Exists)
                        throw new Exception("CacheManager was unable to delete " + oldest.FullName);
                }
                break;
            }
        }

        #endregion

        public FileInfo GetCacheKey(System.Net.WebRequest request)
        {
            MD5 c = MD5.Create();
            Byte []bs = Encoding.UTF8.GetBytes(request.RequestUri.ToString());
            Byte []h = c.ComputeHash(bs);
            String hashAdd = "";

            foreach (byte x in h)
            {
                hashAdd += String.Format("{0:x2}", x);
            }

            return new FileInfo(String.Format("{0}\\{1}_{2}.cache",
                CacheDirectory,
                request.RequestUri.Host,
                hashAdd));
        }

        /// <summary>
        /// Returns the contents from a cache entry.
        /// </summary>
        /// <param name="cacheKey"></param>
        /// <returns></returns>
        public string LoadCacheEntry(FileInfo cacheKey)
        {
            //  Check the in-memory cache first.
            if (LoadedCache.ContainsKey(cacheKey.FullName))
            {
                lock (LoadedCache)
                {
                    MemoryCacheEntry cache = LoadedCache[cacheKey.FullName];

                    cache.Hits++;
                    cache.LastHit = DateTime.Now;

                    return cache.Content;
                }
            }
            else
            {
                //  Fall back to disk
                if (cacheKey.Exists)
                {
                    StreamReader reader = new StreamReader(cacheKey.OpenRead());

                    return StoreInMemory(cacheKey.FullName, reader.ReadToEnd());
                }
                return String.Empty;
            }
        }

        /// <summary>
        /// Stores the data in the memory cache, cleaning up the in-memory cache
        /// as needed to ensure that the memory limit is not exceeded.
        /// </summary>
        /// <param name="key">The filename under which the cached entry would live</param>
        /// <param name="value">The content associated with this key (file contents)</param>
        /// <returns></returns>
        private string StoreInMemory(string key, string value)
        {

            if (MaximumMemoryUsage > 0)
            {
                lock (LoadedCache)
                {
                    LoadedCache[key] = new MemoryCacheEntry()
                    {
                        Content = value,
                        Hits = 0,
                        LastHit = DateTime.Now
                    };
                }
                lock (LoadedCache)
                {
                    int totalMemory = (from l in LoadedCache
                                       select l.Value.Content.Length).Sum();
                    while (totalMemory > MaximumMemoryUsage)
                    {
                        var itemToRemove = (from l in LoadedCache
                                            orderby l.Value.LastHit ascending
                                            select l).First();

                        LoadedCache.Remove(itemToRemove.Key);

                        totalMemory = (from l in LoadedCache
                                       select l.Value.Content.Length).Sum();
                    }
                }
            }

            return value;
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="cacheKey"></param>
        /// <param name="responseSource"></param>
        /// <param name="response"></param>
        internal void WriteCacheEntry(FileInfo cacheKey, string responseSource, System.Net.WebResponse response)
        {
            if (HeaderContains(response.Headers[HttpResponseHeader.CacheControl], "no-cache") ||
                HeaderContains(response.Headers[HttpResponseHeader.Pragma], "no-cache"))
            {
                // If the entry exists already, we'll delete it now...
                LoadedCache.Remove(cacheKey.FullName);
                cacheKey.Delete();
                return;
            }
            try
            {
                StoreInMemory(cacheKey.FullName, responseSource);

                //  If this is not merely an in-memory cache...
                if (MaximumDiskUsage != 0)
                {
                    String lastModified = response.Headers[HttpResponseHeader.LastModified] ??
                        DateTime.Now.ToUniversalTime().ToString();

                    //  Make sure we're not already over quota.
                    ManageSize();

                    using (StreamWriter writer = new StreamWriter(cacheKey.OpenWrite()))
                    {
                        writer.WriteLine(responseSource);
                    }

                    cacheKey.Refresh();

                    if (!String.IsNullOrEmpty(lastModified))
                    {
                        cacheKey.CreationTimeUtc = DateTime.Parse(lastModified);
                        cacheKey.Refresh();
                    }
                }
            }
            catch (Exception ex)
            {
            }
        }

        /// <summary>
        /// Returns true if the header contains the specified substring.
        /// </summary>
        /// <param name="p1"></param>
        /// <param name="p2"></param>
        /// <returns></returns>
        private bool HeaderContains(string p1, string p2)
        {
            if (String.IsNullOrEmpty(p1) || String.IsNullOrEmpty(p2))
                return false;
            else
                return p1.Contains(p2);
        }

        /// <summary>
        /// Determines if the header is equal to the specified value.
        /// </summary>
        /// <param name="p"></param>
        /// <param name="p_2"></param>
        /// <returns></returns>
        private bool HeaderEqual(string p, string p_2)
        {
            if (String.IsNullOrEmpty(p) || String.IsNullOrEmpty(p_2))
                return false;
            else
                return p.Equals(p_2);
        }
    }
}
